/*
 * Copyright (c) 2013 Dalian University of Technology
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation;
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Author: Junling Bu <linlinjavaer@gmail.com>
 */

#include "ns3/config.h"
#include "ns3/data-rate.h"
#include "ns3/mobility-helper.h"
#include "ns3/mobility-model.h"
#include "ns3/ocb-wifi-mac.h"
#include "ns3/packet-socket-address.h"
#include "ns3/packet-socket-client.h"
#include "ns3/packet-socket-helper.h"
#include "ns3/packet-socket-server.h"
#include "ns3/position-allocator.h"
#include "ns3/qos-txop.h"
#include "ns3/rng-seed-manager.h"
#include "ns3/sta-wifi-mac.h"
#include "ns3/string.h"
#include "ns3/test.h"
#include "ns3/vector.h"
#include "ns3/wave-mac-helper.h"
#include "ns3/wifi-80211p-helper.h"
#include "ns3/wifi-net-device.h"
#include "ns3/yans-wifi-helper.h"

#include <iostream>

using namespace ns3;

// helper function to assign streams to random variables, to control
// randomness in the tests
static void
AssignWifiRandomStreams(Ptr<WifiMac> mac, int64_t stream)
{
    int64_t currentStream = stream;
    PointerValue ptr;
    if (!mac->GetQosSupported())
    {
        mac->GetAttribute("Txop", ptr);
        Ptr<Txop> txop = ptr.Get<Txop>();
        currentStream += txop->AssignStreams(currentStream);
    }
    else
    {
        mac->GetAttribute("VO_Txop", ptr);
        Ptr<QosTxop> vo_txop = ptr.Get<QosTxop>();
        currentStream += vo_txop->AssignStreams(currentStream);

        mac->GetAttribute("VI_Txop", ptr);
        Ptr<QosTxop> vi_txop = ptr.Get<QosTxop>();
        currentStream += vi_txop->AssignStreams(currentStream);

        mac->GetAttribute("BE_Txop", ptr);
        Ptr<QosTxop> be_txop = ptr.Get<QosTxop>();
        currentStream += be_txop->AssignStreams(currentStream);

        mac->GetAttribute("BK_Txop", ptr);
        Ptr<QosTxop> bk_txop = ptr.Get<QosTxop>();
        currentStream += bk_txop->AssignStreams(currentStream);
    }
}

/**
 * \ingroup wave-test
 * \ingroup tests
 *
 * \brief Ocb Wifi Mac Test Case
 */
class OcbWifiMacTestCase : public TestCase
{
  public:
    OcbWifiMacTestCase();
    ~OcbWifiMacTestCase() override;

  private:
    void DoRun() override;

    /**
     * MAC associate function
     * \param context the context
     * \param bssid the BSSID
     */
    void MacAssoc(std::string context, Mac48Address bssid);
    /**
     * Phy receive ok trace function
     * \param context the context
     * \param packet the packet
     * \param snr the SNR
     * \param mode the mode
     * \param preamble the preamble
     */
    void PhyRxOkTrace(std::string context,
                      Ptr<const Packet> packet,
                      double snr,
                      WifiMode mode,
                      WifiPreamble preamble);
    /**
     * Phy transmit trace function
     * \param context the context
     * \param packet the packet
     * \param mode the mode
     * \param preamble the preamble
     * \param txPower the transmit power
     */
    void PhyTxTrace(std::string context,
                    Ptr<const Packet> packet,
                    WifiMode mode,
                    WifiPreamble preamble,
                    uint8_t txPower);
    /**
     * Get current position function
     * \param i the current position index
     * \returns the current position vector
     */
    Vector GetCurrentPosition(uint32_t i);
    /**
     * Advance position function
     * \param node the node
     */
    void AdvancePosition(Ptr<Node> node);

    /// Pre random configuration function
    void PreRandomConfiguration();
    /**
     * Configure AP STA mode function
     * \param static_node the static node
     * \param mobile_node the mobile node
     */
    void ConfigureApStaMode(Ptr<Node> static_node, Ptr<Node> mobile_node);
    /**
     * Configure adhoc mode function
     * \param static_node the static node
     * \param mobile_node the mobile node
     */
    void ConfigureAdhocMode(Ptr<Node> static_node, Ptr<Node> mobile_node);
    /**
     * Configure OCB mode function
     * \param static_node the static node
     * \param mobile_node the mobile node
     */
    void ConfigureOcbMode(Ptr<Node> static_node, Ptr<Node> mobile_node);
    /**
     * Post device configuration function
     * \param static_node the static node
     * \param mobile_node the mobile node
     */
    void PostDeviceConfiguration(Ptr<Node> static_node, Ptr<Node> mobile_node);

    Time phytx_time;  ///< Phy transmit time
    Vector phytx_pos; ///< Phy transmit position

    Time macassoc_time;  ///< MAC associate time
    Vector macassoc_pos; ///< MAC associate position

    Time phyrx_time;  ///< Phy receive time
    Vector phyrx_pos; ///< Phy receive position

    // nodes.Get (0) is static node
    // nodes.Get (1) is mobile node
    NodeContainer nodes; ///< the nodes
};

OcbWifiMacTestCase::OcbWifiMacTestCase()
    : TestCase("Association time: Ap+Sta mode vs Adhoc mode vs Ocb mode")
{
}

OcbWifiMacTestCase::~OcbWifiMacTestCase()
{
}

// mobility is like walk on line with velocity 5 m/s
// We prefer to update 0.5m every 0.1s rather than 5m every 1s
void
OcbWifiMacTestCase::AdvancePosition(Ptr<Node> node)
{
    Ptr<MobilityModel> mobility = node->GetObject<MobilityModel>();
    Vector pos = mobility->GetPosition();
    pos.x -= 0.5;
    if (pos.x < 1.0)
    {
        pos.x = 1.0;
        return;
    }
    mobility->SetPosition(pos);

    Simulator::Schedule(Seconds(0.1), &OcbWifiMacTestCase::AdvancePosition, this, node);
}

// here are only two nodes, a stationary and a mobile one
// the i value of the first = 0; the i value of second = 1.
Vector
OcbWifiMacTestCase::GetCurrentPosition(uint32_t i)
{
    NS_ASSERT(i < 2);
    Ptr<Node> node = nodes.Get(i);
    Ptr<MobilityModel> mobility = node->GetObject<MobilityModel>();
    Vector pos = mobility->GetPosition();
    return pos;
}

void
OcbWifiMacTestCase::MacAssoc(std::string context, Mac48Address bssid)
{
    if (macassoc_time == Time(0))
    {
        macassoc_time = Now();
        macassoc_pos = GetCurrentPosition(1);
        std::cout << "MacAssoc time = " << macassoc_time.As(Time::NS)
                  << " position = " << macassoc_pos << std::endl;
    }
}

// We want to get the time that sta receives the first beacon frame from AP
// it means that in this time this sta has ability to receive frame
void
OcbWifiMacTestCase::PhyRxOkTrace(std::string context,
                                 Ptr<const Packet> packet,
                                 double snr,
                                 WifiMode mode,
                                 WifiPreamble preamble)
{
    if (phyrx_time == Time(0))
    {
        phyrx_time = Now();
        phyrx_pos = GetCurrentPosition(1);
        std::cout << "PhyRxOk time = " << phyrx_time.As(Time::NS) << " position = " << phyrx_pos
                  << std::endl;
    }
}

// We want to get the time that STA sends the first data packet successfully
void
OcbWifiMacTestCase::PhyTxTrace(std::string context,
                               Ptr<const Packet> packet,
                               WifiMode mode,
                               WifiPreamble preamble,
                               uint8_t txPower)
{
    WifiMacHeader h;
    packet->PeekHeader(h);
    if ((phytx_time == Time(0)) && h.IsData())
    {
        phytx_time = Now();
        phytx_pos = GetCurrentPosition(1);
        std::cout << "PhyTx data time = " << phytx_time.As(Time::NS) << " position = " << phytx_pos
                  << std::endl;
    }
}

void
OcbWifiMacTestCase::ConfigureApStaMode(Ptr<Node> static_node, Ptr<Node> mobile_node)
{
    YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default();
    YansWifiPhyHelper wifiPhy;
    wifiPhy.SetChannel(wifiChannel.Create());

    Ssid ssid = Ssid("wifi-default");
    WifiMacHelper wifiStaMac;
    wifiStaMac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid));
    WifiMacHelper wifiApMac;
    wifiApMac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid));

    WifiHelper wifi;
    wifi.SetStandard(WIFI_STANDARD_80211p);
    wifi.SetRemoteStationManager("ns3::ConstantRateWifiManager",
                                 "DataMode",
                                 StringValue("OfdmRate6MbpsBW10MHz"),
                                 "ControlMode",
                                 StringValue("OfdmRate6MbpsBW10MHz"));
    wifi.Install(wifiPhy, wifiStaMac, mobile_node);
    wifi.Install(wifiPhy, wifiApMac, static_node);
}

void
OcbWifiMacTestCase::ConfigureAdhocMode(Ptr<Node> static_node, Ptr<Node> mobile_node)
{
    YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default();
    YansWifiPhyHelper wifiPhy;
    wifiPhy.SetChannel(wifiChannel.Create());

    WifiMacHelper wifiMac;
    wifiMac.SetType("ns3::AdhocWifiMac");

    WifiHelper wifi;
    wifi.SetStandard(WIFI_STANDARD_80211p);
    wifi.SetRemoteStationManager("ns3::ConstantRateWifiManager",
                                 "DataMode",
                                 StringValue("OfdmRate6MbpsBW10MHz"),
                                 "ControlMode",
                                 StringValue("OfdmRate6MbpsBW10MHz"));
    wifi.Install(wifiPhy, wifiMac, mobile_node);
    wifi.Install(wifiPhy, wifiMac, static_node);
}

void
OcbWifiMacTestCase::ConfigureOcbMode(Ptr<Node> static_node, Ptr<Node> mobile_node)
{
    YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default();
    YansWifiPhyHelper wifiPhy;
    wifiPhy.SetChannel(wifiChannel.Create());

    NqosWaveMacHelper wifi80211pMac = NqosWaveMacHelper::Default();

    Wifi80211pHelper wifi80211p = Wifi80211pHelper::Default();
    wifi80211p.SetRemoteStationManager("ns3::ConstantRateWifiManager",
                                       "DataMode",
                                       StringValue("OfdmRate6MbpsBW10MHz"),
                                       "ControlMode",
                                       StringValue("OfdmRate6MbpsBW10MHz"));
    wifi80211p.Install(wifiPhy, wifi80211pMac, mobile_node);
    wifi80211p.Install(wifiPhy, wifi80211pMac, static_node);
}

void
OcbWifiMacTestCase::PostDeviceConfiguration(Ptr<Node> static_node, Ptr<Node> mobile_node)
{
    Ptr<WifiNetDevice> static_device = DynamicCast<WifiNetDevice>(static_node->GetDevice(0));
    Ptr<WifiNetDevice> mobile_device = DynamicCast<WifiNetDevice>(mobile_node->GetDevice(0));

    // Fix the stream assignment to the Dcf Txop objects (backoffs)
    // The below stream assignment will result in the Txop object
    // using a backoff value of zero for this test when the
    // Txop::EndTxNoAck() calls to StartBackoffNow()
    AssignWifiRandomStreams(static_device->GetMac(), 21);
    AssignWifiRandomStreams(mobile_device->GetMac(), 22);

    // setup mobility
    // the initial position of static node is at 0,
    // and the initial position of mobile node is 350.
    MobilityHelper mobility;
    mobility.Install(mobile_node);
    mobility.Install(static_node);
    Ptr<MobilityModel> mm = mobile_node->GetObject<MobilityModel>();
    Vector possta = mm->GetPosition();
    possta.x = 350;
    mm->SetPosition(possta);
    Simulator::Schedule(Seconds(1.0), &OcbWifiMacTestCase::AdvancePosition, this, mobile_node);

    PacketSocketAddress socket;
    socket.SetSingleDevice(mobile_device->GetIfIndex());
    socket.SetPhysicalAddress(static_device->GetAddress());
    socket.SetProtocol(1);

    // give packet socket powers to nodes.
    PacketSocketHelper packetSocket;
    packetSocket.Install(static_node);
    packetSocket.Install(mobile_node);

    Ptr<PacketSocketClient> client = CreateObject<PacketSocketClient>();
    client->SetRemote(socket);
    mobile_node->AddApplication(client);
    client->SetStartTime(Seconds(0.5));
    client->SetStopTime(Seconds(70.0));

    Ptr<PacketSocketServer> server = CreateObject<PacketSocketServer>();
    server->SetLocal(socket);
    static_node->AddApplication(server);
    server->SetStartTime(Seconds(0.0));
    server->SetStopTime(Seconds(70.5));

    phytx_time = macassoc_time = phyrx_time = Time();
    phytx_pos = macassoc_pos = phyrx_pos = Vector();

    if (DynamicCast<StaWifiMac>(mobile_device->GetMac()))
    {
        // This trace is available only in a StaWifiMac
        Config::Connect("/NodeList/1/DeviceList/*/Mac/Assoc",
                        MakeCallback(&OcbWifiMacTestCase::MacAssoc, this));
    }
    Config::Connect("/NodeList/1/DeviceList/*/Phy/State/RxOk",
                    MakeCallback(&OcbWifiMacTestCase::PhyRxOkTrace, this));
    Config::Connect("/NodeList/1/DeviceList/*/Phy/State/Tx",
                    MakeCallback(&OcbWifiMacTestCase::PhyTxTrace, this));
}

/**
 *
 *   static-node:0    <----       mobile-node:1
 *        *   ------ 350m -------    *
 *
 * the node transmit range is less than 150m
 *
 * Ap+Sta mode vs Adhoc mode vs Ocb mode
 * first test the time point when the stationary node is
 * an AP and the mobile node is a Sta
 * then test when one Ad-hoc node and another Ad-hoc node
 * last test when one OCB node and another OCB node
 */
void
OcbWifiMacTestCase::DoRun()
{
    std::cout << "test time point for Ap-Sta mode" << std::endl;
    PreRandomConfiguration();
    nodes = NodeContainer();
    nodes.Create(2);
    Ptr<Node> static_node = nodes.Get(0);
    Ptr<Node> mobile_node = nodes.Get(1);
    ConfigureApStaMode(static_node, mobile_node);
    PostDeviceConfiguration(static_node, mobile_node);
    Simulator::Stop(Seconds(71.0));
    Simulator::Run();
    Simulator::Destroy();
    NS_TEST_ASSERT_MSG_LT(
        phyrx_time,
        macassoc_time,
        "In Sta mode with AP, you cannot associate until receive beacon or AssocResponse frame");
    NS_TEST_ASSERT_MSG_LT(macassoc_time,
                          phytx_time,
                          "In Sta mode with AP,  you cannot send data packet until associate");
    // Are these position tests redundant with time check tests?
    // NS_TEST_ASSERT_MSG_GT ((phyrx_pos.x - macassoc_pos.x), 0.0, "");
    // actually macassoc_pos.x - phytx_pos.x is greater than 0
    // however associate switch to send is so fast with less than 100ms
    // and in our mobility model that every 0.1s update position,
    // so turn out to be that macassoc_pos.x - phytx_pos.x is equal to 0
    // NS_TEST_ASSERT_MSG_GT ((macassoc_pos.x - phytx_pos.x), 0.0, "");

    std::cout << "test time point for Adhoc mode" << std::endl;
    PreRandomConfiguration();
    nodes = NodeContainer();
    nodes.Create(2);
    static_node = nodes.Get(0);
    mobile_node = nodes.Get(1);
    ConfigureAdhocMode(static_node, mobile_node);
    PostDeviceConfiguration(static_node, mobile_node);
    Simulator::Stop(Seconds(71.0));
    Simulator::Run();
    Simulator::Destroy();
    // below test assert will fail, because AdhocWifiMac has not implement state machine.
    // if someone takes a look at the output in adhoc mode and in Ocb mode
    // he will find these two outputs are almost same.
    // NS_TEST_ASSERT_MSG_LT (phyrx_time, macassoc_time, "In Adhoc mode, you cannot associate until
    // receive beacon or AssocResponse frame" ); NS_TEST_ASSERT_MSG_LT (macassoc_time, phytx_time,
    // "In Adhoc mode,  you cannot send data packet until associate" ); NS_TEST_ASSERT_MSG_GT
    // ((phyrx_pos.x - macassoc_pos.x), 0.0, "");
    // below test assert result refer to Ap-Sta mode
    // NS_TEST_ASSERT_MSG_GT ((macassoc_pos.x - phytx_pos.x), 0.0, "");

    std::cout << "test time point for Ocb mode" << std::endl;
    PreRandomConfiguration();
    nodes = NodeContainer();
    nodes.Create(2);
    static_node = nodes.Get(0);
    mobile_node = nodes.Get(1);
    ConfigureOcbMode(static_node, mobile_node);
    PostDeviceConfiguration(static_node, mobile_node);
    Simulator::Stop(Seconds(71.0));
    Simulator::Run();
    Simulator::Destroy();
    NS_TEST_ASSERT_MSG_EQ(macassoc_time.GetNanoSeconds(),
                          0,
                          "In Ocb mode, there is no associate state machine");
    NS_TEST_ASSERT_MSG_LT(phytx_time,
                          phyrx_time,
                          "before mobile node receives frames from far static node, it can send "
                          "data packet directly");
    NS_TEST_ASSERT_MSG_EQ(macassoc_pos.x, 0.0, "");
    NS_TEST_ASSERT_MSG_GT((phytx_pos.x - phyrx_pos.x), 0.0, "");
}

void
OcbWifiMacTestCase::PreRandomConfiguration()
{
    // Assign a seed and run number, and later fix the assignment of streams to
    // WiFi random variables, so that the first backoff used is zero slots
    RngSeedManager::SetSeed(1);
    RngSeedManager::SetRun(17);
    // the WiFi random variables is set in PostDeviceConfiguration method.
}

/**
 * \ingroup wave-test
 * \ingroup tests
 *
 * \brief Ocb Test Suite
 */
class OcbTestSuite : public TestSuite
{
  public:
    OcbTestSuite();
};

OcbTestSuite::OcbTestSuite()
    : TestSuite("wave-80211p-ocb", UNIT)
{
    // TestDuration for TestCase can be QUICK, EXTENSIVE or TAKES_FOREVER
    AddTestCase(new OcbWifiMacTestCase, TestCase::QUICK);
}

// Do not forget to allocate an instance of this TestSuite
static OcbTestSuite ocbTestSuite; ///< the test suite
